package edu.zju.udms.dnavigator.core.entity;

import static edu.zju.udms.dnavigator.core.utils.ClassUtils.getAllFields;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;

public class MetaLuceneMapper {
	private static final Logger logger=Logger.getLogger(MetaLuceneMapper.class);

	private Map<Class<? extends MetaObject>,Map<ClassInfoEnum,Object>> classInfo=new HashMap<>();
	
	private MetaLuceneMapper(){}
	private static MetaLuceneMapper mlp=new MetaLuceneMapper();
	public static MetaLuceneMapper getInstance(){
		return mlp;
	}
	
	private synchronized void parseClass(Class<? extends MetaObject> clazz){
		logger.debug(String.format("get class: %s infomation",clazz.getCanonicalName()));
		Field[] fields=getAllFields(clazz,MetaObject.class);
		Set<Field> keyField=new HashSet<>();
		Set<Field> versionField=new HashSet<>();
		Set<Field> normalField=new HashSet<>();
		Set<Field> normalAnalyzedField=new HashSet<>();
		for(Field f : fields){
			AttributeType attributeType = f.getAnnotation(AttributeType.class);
			if(attributeType!=null){
				f.setAccessible(true);
				AttributeTypeEnum typeEnum = attributeType.value();
				switch (typeEnum) {
				case Key:
					keyField.add(f);
					break;
				case Version:
					versionField.add(f);
					break;
				case Normal:
					normalField.add(f);
					break;
				case Normal_Analyzed:
					normalAnalyzedField.add(f);
					break;
				default:
					break;
				}
				logger.debug(String.format("field<%s> is %s attribute",f.getName(),typeEnum.name()));
			}
		}
		Map<ClassInfoEnum,Object> map=new HashMap<>();
		map.put(ClassInfoEnum.KeyField,keyField);
		map.put(ClassInfoEnum.VersionField,versionField);
		map.put(ClassInfoEnum.NormalField,normalField);
		map.put(ClassInfoEnum.NormalAnalyzedField,normalAnalyzedField);
		classInfo.put(clazz, map);
	}
	
	public Map<String,String> doc2map(Document doc){
		Map<String,String> map=new HashMap<>();
		List<IndexableField> fields = doc.getFields();
		for(IndexableField f:fields){
			String fname = f.name();
			map.put(fname, doc.get(fname));
		}
		return map;
	}
	
	@SuppressWarnings("unchecked")
	public MetaObject doc2meta(Document doc){
		String classname = doc.get("_classname");
		Class<? extends MetaObject> clazz;
		MetaObject meta;
		if (classname==null)
			return null;
		try {
			clazz = (Class<? extends MetaObject>) Class.forName(classname);
		} catch (ClassNotFoundException e) {
			logger.error("",e);
			return null;
		}
		try {
			meta=clazz.newInstance();
		} catch (Exception e) {
			logger.error("MetaObject can be create",e);
			return null;
		}
		Map<ClassInfoEnum, Object> info = getClassInfo(clazz);
		meta.setId(doc.get("_id"));
		meta.setVersion(Long.parseLong(doc.get("_version")));
		for(ClassInfoEnum ckey:ClassInfoEnum.values()){
			if(ckey.name().endsWith("Field")){
				Set<Field> fields=(Set<Field>) info.get(ckey);
				for(Field f : fields){
					try {
						f.set(meta, string2field(f,doc.get(f.getName())));
					} catch (Exception e) {
						logger.error("",e);
					}
				}
			}
		}
		return meta;
	}
	
	@SuppressWarnings("unchecked")
	public Document meta2doc(MetaObject meta){
		Document doc=new Document();
		long version = meta.getVersion();
		Store store = org.apache.lucene.document.Field.Store.YES;
		doc.add(new StringField("_id", meta.getId(),store));
		doc.add(new LongField("_version", version,store));
		doc.add(new NumericDocValuesField("_version",version ));
		doc.add(new StringField("_classname", meta.getClassname(),store));
		Map<ClassInfoEnum, Object> info = getClassInfo(meta.getClass());
		StringBuilder fulltext=new StringBuilder();
		for(ClassInfoEnum ckey:ClassInfoEnum.values()){
			Set<Field> fields=(Set<Field>) info.get(ckey);
			for(Field f : fields){
				String s;
				try {
					s=field2string(f,f.get(meta));
				} catch (Exception e) {
					s="";
					logger.error("",e);
				}
				fulltext.append(s);
				if(ckey==ClassInfoEnum.NormalAnalyzedField){
					doc.add(new TextField(f.getName(), s, store));
				}else{
					doc.add(new StringField(f.getName(), s,store));
				}
			}
		}
		doc.add(new TextField("_fulltext",fulltext.toString(), org.apache.lucene.document.Field.Store.NO));
		return doc;
	}
	
	/** this method generate lucene query according 
	 *  to key field
	 * */
	@SuppressWarnings("unchecked")
	public Query meta2query(MetaObject meta){
		BooleanQuery query=new BooleanQuery();
		Map<ClassInfoEnum, Object> info = getClassInfo(meta.getClass());
		Set<Field> keyFields = (Set<Field>)info.get(ClassInfoEnum.KeyField);
		for(Field f:keyFields){
			String s;
			try {
				s = field2string(f, f.get(meta));
			} catch (Exception e) {
				logger.error("",e);
				s="";
			}
			query.add(new TermQuery(new Term(f.getName(),s)), BooleanClause.Occur.MUST);
		}
		return query;
	}
	
	public Query id2query(String id){
		return new TermQuery(new Term("_id",id.toString()));
	}
	
	public Query generateQuery(Map<String,Object> map){
		BooleanQuery query=new BooleanQuery();
		for(String k:map.keySet()){
			query.add(new TermQuery(new Term(k,map.get(k).toString())), BooleanClause.Occur.MUST);
		}
		return query;
	}
	
	@SuppressWarnings("unchecked")
	public boolean isIdentical(MetaObject a,MetaObject b){
		if(a==null||b==null){
			if(a==null&&b==null)
				return true;
			else
				return false;
		}
		Class<? extends MetaObject> clazz = a.getClass();
		if(!clazz.equals(b.getClass()))
			return false;
		Set<Field> fields=(Set<Field>)getClassInfo(clazz).get(ClassInfoEnum.KeyField);
		return fieldsAllEqual(fields,a,b);
	}
	
	/**caller must call isIdentical method to determine 
	 * whether two objects are identical 
	 * @see isIdentical
	 * */
	@SuppressWarnings("unchecked")
	public boolean isVersioned(MetaObject a,MetaObject b){
		if(a==null||b==null){
			if(a==null&&b==null)
				return true;
			else
				return false;
		}
		Class<? extends MetaObject> clazz = a.getClass();
		if(!clazz.equals(b.getClass()))
			return false;
		Set<Field> fields=(Set<Field>)getClassInfo(clazz).get(ClassInfoEnum.VersionField);
		return fieldsAllEqual(fields,a,b);
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private Object string2field(Field f,String s){
		if(s==null||s.length()==0)
			return null;
		Class fClass = f.getType();
		Class fSuperClass = fClass.getSuperclass();
		String fClassName=fClass.getName(),fSuperName=null;
		if(fSuperClass!=null){
			fSuperName=fSuperClass.getName();
		}else{
			fSuperName="";
		}
		
		if(fClassName.equals("int")){
			return Integer.parseInt(s);
		}else if(fClassName.equals("long")){
			return Long.parseLong(s);
		}else if(fClassName.equals("float")){
			return Float.parseFloat(s);
		}else if(fClassName.equals("double")){
			return Double.parseDouble(s);
		}else if(fClassName.equals("java.lang.String")){
			return s;
		}else if(fSuperName.equals("java.lang.Enum")){
			return Enum.valueOf(fClass, s);
		}else
			return null;
	}
	
	private String field2string(Field f,Object o){
		if(o==null)
			return "";
		return o.toString();
	}
	
	private Map<ClassInfoEnum,Object> getClassInfo(Class<? extends MetaObject> clazz){
		if(!classInfo.containsKey(clazz)){
			parseClass(clazz);
		}
		return classInfo.get(clazz);
	}
	
	private boolean fieldsAllEqual(Collection<Field> fields,Object a,Object b){
		boolean ok = true;
		for(Field f:fields){
			try {
				Object fav = f.get(a);
				Object fbv = f.get(b);
				if(fav instanceof String&&(((String) fav).length()==0)){
					fav=null;
				}
				if(fbv instanceof String&&(((String) fbv).length()==0)){
					fbv=null;
				}
				if(fav==null||fbv==null){
					if(fav==null&&fbv==null){
						continue;
					}else{
						ok=false;
						break;
					}
				}
				if(!fav.equals(fbv)){
					ok=false;
					break;
				}
			} catch (Exception e) {
				logger.error("",e);
				ok=false;
				break;
			}
		}
		return ok;
	}
}

enum ClassInfoEnum{
	KeyField,
	VersionField,
	NormalField,
	NormalAnalyzedField,
}